package net;

import exceptions.TransferCollapsedException;
import interfaces.Core_Events;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketTimeoutException;
import java.nio.BufferOverflowException;


/**
 *
 * @author Groš
 */

/** Second part logic - focused on server logic */
public class UDP_Server {

    private Core_Events event;
    private DatagramSocket socket;
    
    /** packet buffer state - shows where are received rights packets 
     * <code> 1 </code> = packet received
     * <code> 0 </code> = packet NOT received
     */
    private int[] r_buffer_state = new int[core.Core.WINDOW_LENGTH];
    
    private byte[][] r_buffer_b = new byte[core.Core.WINDOW_LENGTH][core.Core.DATAGRAM_LENGTH];
    
    /** packets buffer */
    private DatagramPacket[] r_buffer = new DatagramPacket[core.Core.WINDOW_LENGTH];
    
    
    private byte[] ackPacket_b = new byte[core.Core.DATAGRAM_LENGTH];
    /** acknowledge packet */
    private DatagramPacket ackPacket = new DatagramPacket(ackPacket_b, ackPacket_b.length);
    
    private byte[] rPacket_b = new byte[core.Core.DATAGRAM_LENGTH];
    /** packet received and processed */
    private DatagramPacket rPacket = new DatagramPacket(rPacket_b, rPacket_b.length);
    
    private int ackID = 0;
    private int pID = 0;
    
    /** minimal awaited packet ID */
    private int pID_min = 1;
    
    /** maximal awaited packet ID */
    private int pID_max = (pID_min - 1) + core.Core.WINDOW_LENGTH;
    
    /** buffer offset - index where to store packets in packet buffer */
    private int offset = 0;
    
    private int timeout_hops = 0;
    private int save_position = 0;
    
    private boolean stop = false;
    private boolean packetReceived = false;
    private long dataReceived = 0;
    private int tryes = 0;

    /** Second part logic - focused on server logic 
     * initiates packet buffer and reset packet buffer state
     * @param socket socket where to receive packets 
     * @param event needed interface to communicate with <code>Core</code>
     */
    public UDP_Server(DatagramSocket socket, Core_Events event) {
        this.socket = socket;
        this.event = event;
        initPackets();
        resetBufferState();
    }
    
    /** create new packet buffer with pre-prepared packets */
    private void initPackets() {
        for (int i=0; i < r_buffer.length; i++) {
            r_buffer[i] = new DatagramPacket(r_buffer_b[i], r_buffer_b[i].length);
        }
    }
    
    /** reset packet buffer state - writes <code> 0 </code> */
    private void resetBufferState(){
        for (int i=0; i<r_buffer_state.length; i++)
            r_buffer_state[i] = 0;
    }
    
    /** receives packets
     * @throws TransferCollapsedException
     * @throws IOException
     */
    public void receivePackets() 
            throws TransferCollapsedException, IOException{
        timeout_hops = 0;
        packetReceived = false;
        
        // try to fill whole WINDOW_LENGTH
        while(timeout_hops < core.Core.WINDOW_LENGTH && !reachedLastBufferPosition() && !stop){
            try {
                // receive packet
                socket.receive(rPacket);
                pID = UDP_Core.udp_getPacketID(rPacket);
                offset = (pID > 0) ? pID - pID_min : offset;
                
                // check if this packets is awaited
                if (pID >= pID_min && pID <= pID_max) {
                    addToBuffer(rPacket, offset);
                    event.core_message("UDP:SD>" + dataReceived);
                }
                if (pID == UDP_Core.ACK.FIN.value()) {
                    addToBuffer(rPacket, 0); // receive packet always must be at 0 position
                }
            }
            catch(SocketTimeoutException stex) {
                timeout_hops++;
                continue;
            }
        }

        // if buffer is empty - no new packet received
        if (!packetReceived) {              
            tryes++;
            if (tryes == core.Core.RETRANSMIT_COUNTER && pID != 0)
                throw new TransferCollapsedException();
            
            // send last good acknoledge
            send_AckID(ackID);
            
            // try receive packets
            receivePackets();
            return;
        }
        else {
            tryes = 0;
            timeout_hops = 0;
            // count new acknoledge ID
            count_AckID();
            
            // send acknoledge ID
            send_AckID(ackID);
            
            // change minimum and maximum for awaited packets
            pID_min = ackID + 1;
            pID_max = (pID_min - 1) + core.Core.WINDOW_LENGTH;
        }        
    }
    
    /** if <code>offset</code> reaches to packet buffer end
     * @return true | false
     */
    private boolean reachedLastBufferPosition() {
        return (r_buffer_state[r_buffer_state.length-1] == 1 && r_buffer_state[0] == 1) ? true : false;
    }
   
    /** sends counted acknowledge ID 
     * @throws IOException
     */
    public void send_AckID() throws IOException {
        send_AckID(ackID);
    }
    
    /** sends acknoledge ID
     * @param id ID to send
     * @throws IOException
     */
    public void send_AckID(int id) throws IOException {
        if (ackPacket.getAddress() != null) {
            event.core_message("UDP:S>Sendind ACK=" + id);
            ackPacket.setData(UDP_Core.udp_intToBytes(id));
            socket.send(ackPacket);
        }
    }
    
    /** returns packet buffer 
     * @return packet buffer
     */
    public DatagramPacket[] getPackets() {
            return r_buffer;
    }
    
    /** returns position in a buffer where are received good packets
     * |P|X|P|P| - returns 1
     * |X|P|P|P| - returns 0
     * |P|P|P|X| - returns 3
     * @return position
     */
    public int getSavePosition(){
        return save_position;
    }
    
    /** add packet to buffer and sets internal states 
     * @param packet source
     * @param index position in buffer
     */
    private void addToBuffer(DatagramPacket packet, int index){
        if(index < 0 || index >= r_buffer.length)
            throw new BufferOverflowException();
        
        event.core_message("UDP:S>Received Datagram ID:" + pID + 
                "; Source: IP=" + packet.getAddress().getHostAddress() + 
                " port=" + packet.getPort());
        copyPacket(packet, r_buffer[index]);
        
        dataReceived += packet.getLength();
        r_buffer_state[index] = 1;
        ackPacket.setAddress(packet.getAddress());
        ackPacket.setPort(packet.getPort());
        packetReceived = true;
    }
    
    /** move packets in a packet buffer
     * @param leftSteps - about how many fields move
     */
    public void moveBuffer(int leftSteps) {
        for(int i = 0; i < r_buffer.length; i++)
            
            if (i + leftSteps < r_buffer.length) {
                copyPacket(r_buffer[i + leftSteps], r_buffer[i]);
                r_buffer_state[i] = r_buffer_state[i + leftSteps];
            }
            else 
                r_buffer_state[i] = 0;
        offset = r_buffer.length - leftSteps;
    }
    
    /** clones packet 
     * @param source  source packet
     * @param dest destination packet
     */
    private void copyPacket(DatagramPacket source, DatagramPacket dest) {
        dest.setData(source.getData().clone());
        dest.setLength(source.getLength());
        dest.setAddress(source.getAddress());
        if (source.getPort() != -1)
            dest.setPort(source.getPort());
    }
    
    /** counts acknoledge ID */
    private void count_AckID(){
        for (int i=0; i < r_buffer_state.length; i++){
            save_position = i;
            
            // |X|P|P|P|
            if (r_buffer_state[i] == 0 && i == 0) {
               break;
            }
            
            // |P|P|X|P|
            if (r_buffer_state[i] == 0 && i > 0) {
                ackID = UDP_Core.udp_getPacketID(r_buffer[i-1]);
                break;
            }
            
            // |P|P|P|P|
            if (r_buffer_state[i] == 1 && i == (r_buffer_state.length-1)) {
                ackID = UDP_Core.udp_getPacketID(r_buffer[i]);
                save_position = i + 1;
            }
        }
    }
    
    /** stops process of receiving packets 
     * @param stop true - stop
     */
    public void setStop(boolean stop) {
        this.stop = stop;
        dataReceived = 0;
    }
    
    /** resets internal states variables */
    public void reset(){
        ackID = 0;
        pID = 0;
        pID_min = 1;
        pID_max = (pID_min - 1) + core.Core.WINDOW_LENGTH;
        offset = 0;
        timeout_hops = 0;
        save_position = 0;
        stop = false;
        dataReceived = 0;
        tryes = 0;
        resetBufferState();
        ackPacket.setAddress(null);
    }
    
}
